/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.painting;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.BufferedHttpEntity;
import org.apache.http.impl.client.DefaultHttpClient;

import android.graphics.Bitmap;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.ApplicationData;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.handlers.events.EventRaisingException;
import com.hercules.android.receiver.model.events.XREResourceEvent;
import com.hercules.android.receiver.model.objects.XREFont;
import com.hercules.android.receiver.model.objects.XREImage;
import com.hercules.android.receiver.model.objects.XREResourceOptions;
import com.hercules.android.receiver.model.objects.XREText;
import com.hercules.android.receiver.model.objects.XRETextWrap;
import com.hercules.android.receiver.model.objects.XREView;
import com.hercules.android.receiver.painting.views.AndroidXREView;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.UnknownServiceException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

/**
 * <p>
 * Helper methods used in this component. None of them should be used outside of this component.
 * </p>
 * <p>
 * AndroidAssembly1 changed name, also some bugs fixed
 * <p>
 * <b> Thread Safety: </b> It's immutable and thread safe.
 * </p>
 *
 * @author EveningSun
 * @author 7realm
 * @version 1.0
 */
public class PaintersUtil {
    /**
     * <p>
     * Private constructor.
     * </p>
     */
    private PaintersUtil() {
        // empty
    }

    /**
     * <p>
     * Check whether the given object is null. Throws IllegalArgumentException if so.
     * </p>
     *
     * @param object the object to be checked
     * @param name the name of the object showed in error message
     * @throws IllegalArgumentException if the given object is null
     */
    public static void checkNull(Object object, String name) {
        if (object == null) {
            throw new IllegalArgumentException("The parameter " + name + " cannot be null.");
        }
    }

    /**
     * <p>
     * Check whether the given object is null. Throws IllegalStateException if so.
     * </p>
     *
     * @param object the object to be checked
     * @param name the name of the object showed in error message
     * @throws IllegalStateException if the given object is null
     */
    public static void checkState(Object object, String name) {
        if (object == null) {
            throw new IllegalStateException("The " + name + " cannot be null.");
        }
    }

    /**
     * <p>
     * Get the painter data of the specified key in the given receiver. The retrieved painter data would be a map.
     * </p>
     *
     * @param <K> the key type of the retrieved painter data
     * @param <V> the value type of the retrieved painter data
     * @param receiver the android receiver
     * @param dataMapKey the data map key
     * @return the painter data
     * @throws XREObjectPainterException if any data missing in the receiver, or if the painter data is not a map
     */
    @SuppressWarnings("unchecked")
    public static <K, V> Map<K, V> getPainterDataMap(AndroidReceiver receiver, String dataMapKey)
        throws XREObjectPainterException {
        ApplicationData applicationData = required(receiver.getApplicationData(), "applicationData");
        Map<String, Object> extraPainterData = required(applicationData.getExtraPainterData(), "extraPainterData");
        try {
            return (Map<K, V>) required(extraPainterData.get(dataMapKey), "extraPainterData#" + dataMapKey);
        } catch (ClassCastException e) {
            throw new XREObjectPainterException("The extra painter data with the key '" + dataMapKey
                + "' is not an instance of Map.", e);
        }
    }

    /**
     * <p>
     * Get the value corresponding to the specified key of the map in the painter data in the given receiver.
     * </p>
     *
     * @param <K> the key type of the retrieved painter data
     * @param <V> the value type of the retrieved painter data
     * @param receiver the receiver
     * @param dataMapKey the key of the map in painter data
     * @param dataKey the key of the value in the map
     * @param type the expected type of the value
     * @return the value
     * @throws XREObjectPainterException if any data missing in the receiver, or if the painter data is not a map, or if
     * the retrieved value is not the expected type
     */
    public static <K, V> V getPainterDataValue(AndroidReceiver receiver, String dataMapKey, K dataKey, Class<V> type)
        throws XREObjectPainterException {
        try {
            return type.cast(getPainterDataMap(receiver, dataMapKey).get(dataKey));
        } catch (ClassCastException e) {
            throw new XREObjectPainterException("In the extraPainterData map '" + dataMapKey
                + "', the value corresponds to key '" + dataKey + "' is not an instance of " + type, e);
        }
    }

    /**
     * <p>
     * Check whether the given object is null. Throw XREObjectPainterException if so, otherwise, simply return the
     * object.
     * </p>
     *
     * @param <T> the type of the object
     * @param object the object
     * @param name the name of the object showed in error message
     * @return the object
     * @throws XREObjectPainterException if the object is null
     */
    public static <T> T required(T object, String name) throws XREObjectPainterException {
        if (object == null) {
            throw new XREObjectPainterException("The " + name + " cannot be null.");
        }

        return object;
    }

    /**
     * Calculate the target rectangle according to the resource options.
     * <p>
     * AndroidAssembly1 fixed bugs for stretch, splitted to two methods
     *
     * @param xreView the XREView which contains the resource options
     * @param sourceWidth the width of the source that will be drawn
     * @param sourceHeight the height of the source that will be drawn
     * @return the target rectangle
     */
    public static Rect calculateStretchAndAling(XREView xreView, int sourceWidth, int sourceHeight) {
        if (xreView.getResourceOptions() == null) {
            return new Rect(0, 0, sourceWidth, sourceHeight);
        }

        // get canvas dimensions
        int canvasWidth = xreView.getWidth();
        int canvasHeight = xreView.getHeight();

        // calculate image stretch
        if (xreView.getResourceOptions().getStretch() != null) {
            switch (xreView.getResourceOptions().getStretch()) {
            case FIT_WIDTH:
                sourceHeight *= (float) canvasWidth / sourceWidth;
                sourceWidth = canvasWidth;
                break;
            case FIT_HEIGHT:
                sourceWidth *= (float) canvasHeight / sourceHeight;
                sourceHeight = canvasHeight;
                break;
            case FILL:
                sourceWidth = canvasWidth;
                sourceHeight = canvasHeight;
                break;
            case FIT_BEST:
                if ((float) sourceWidth / canvasWidth < (float) sourceHeight / canvasHeight) {
                    sourceWidth *= (float) canvasHeight / sourceHeight;
                    sourceHeight = canvasHeight;
                } else {
                    sourceHeight *= (float) canvasWidth / sourceWidth;
                    sourceWidth = canvasWidth;
                }
                break;
            case FILL_WITH_CLIP:
                if ((float) sourceWidth / canvasWidth < (float) sourceHeight / canvasHeight) {
                    sourceHeight *= (float) canvasWidth / sourceWidth;
                    sourceWidth = canvasWidth;
                } else {
                    sourceWidth *= (float) canvasHeight / sourceHeight;
                    sourceHeight = canvasHeight;
                }
                break;
            default:
                // NONE
                break;
            }
        }

        return calculateAlign(xreView, sourceWidth, sourceHeight, canvasWidth, canvasHeight);
    }

    /**
     * Calculate the target rectangle according to the resource options.
     * <p>
     * AndroidAssembly1 fixed bugs for align, splitted to two methods
     *
     * @param xreView the XREView which contains the resource options
     * @param sourceWidth the width of the source that will be drawn
     * @param sourceHeight the height of the source that will be drawn
     * @param canvasWidth the canvas width
     * @param canvasHeight the canvas height
     * @return the target rectangle
     */
    public static Rect calculateAlign(XREView xreView, int sourceWidth, int sourceHeight, int canvasWidth,
        int canvasHeight) {
        if (xreView.getResourceOptions() == null) {
            return new Rect(0, 0, sourceWidth, sourceHeight);
        }

        // set default value for the image target rectangle
        int left = 0;
        int right = sourceWidth;
        int top = 0;
        int bottom = sourceHeight;

        // calculate horizontal align, default to left, so we don't need to process left
        if (xreView.getResourceOptions().getHorizontalAlign() != null) {
            switch (xreView.getResourceOptions().getHorizontalAlign()) {
            case RIGHT:
                right = canvasWidth;
                left = right - sourceWidth;
                break;
            case CENTER:
                left = (canvasWidth - sourceWidth) / 2;
                right = left + sourceWidth;
                break;
            default:
                break;
            }
        }

        // calculate vertical align, default to top, so we don't need to process top
        if (xreView.getResourceOptions().getVerticalAlign() != null) {
            switch (xreView.getResourceOptions().getVerticalAlign()) {
            case BOTTOM:
                bottom = canvasHeight;
                top = bottom - sourceHeight;
                break;
            case CENTER:
                top = (canvasHeight - sourceHeight) / 2;
                bottom = top + sourceHeight;
                break;
            default:
                break;
            }
        }

        return new Rect(left, top, right, bottom);
    }

    private static Map<String, ImageData> imageMap = Collections.synchronizedMap(new HashMap<String, ImageData>());
    
    private static List<String> pendingImageUrls = Collections.synchronizedList(new ArrayList<String>());
    
    private static AtomicInteger imageLoadingThreadCount = new AtomicInteger(0);
    
    public static final int MAX_IMAGE_LOAD_THREAD_NUM = 3;

    
    /**
     * Load image and register view to it.
     * <p>
     * AndroidAssembly3 added {@link XREImage} notification after loading
     *
     * @param view the view that is related to image
     * @param imageUrl the image url
     * @param receiver the receiver to use
     * @param xreImage the {@link XREImage} that should be notified after image is loaded
     * @throws XREObjectPainterException if error occurs while loading
     */
    @SuppressWarnings("unchecked")
	public static void loadImage(String imageUrl, AndroidReceiver receiver, XREImage xreImage)
        throws XREObjectPainterException {
    	Map<String, Object> extraData = receiver.getApplicationData().getExtraPainterData();
    	List<XREImage> xreImages = (List<XREImage>) extraData.get("pendingImages");
    	
        // check if image was requested before
        if (!imageMap.containsKey(imageUrl)) {
        	ImageData imageData = new ImageData();
        	imageData.setReceiver(receiver);
        	
            imageMap.put(imageUrl, imageData);

            pendingImageUrls.add(imageUrl);
            xreImages.add(xreImage);

            if (imageLoadingThreadCount.get() < MAX_IMAGE_LOAD_THREAD_NUM) {
            	new ImageLoadingThread().start();
            }
        } else {
            // AndroidAssembly3 notify on ready if Bitmap is loaded
            // image is loaded
        	/*ImageData imageData = imageMap.get(imageUrl);
        	
    		if (imageData.isLoadComplete()) {
                // raise onReady event
                XREResourceEvent event = new XREResourceEvent();
                event.setName(XREResourceEvent.ON_READY_EVENT_NAME);
                event.setSource(xreImage.getId());
                try {
                    AndroidXREView.getEventRaiser().raiseEvent(event, receiver, true);
                } catch (EventRaisingException e) {
                    throw new XREObjectPainterException(
                        "Error occurred while raising event 'onReady' event for XREImage.", e);
                }
            } else {
            	xreImages.add(xreImage);
            }*/
        }
        
        XREResourceEvent event = new XREResourceEvent();
        event.setName(XREResourceEvent.ON_READY_EVENT_NAME);
        event.setSource(xreImage.getId());
        try {
            AndroidXREView.getEventRaiser().raiseEvent(event, receiver, true);
        } catch (EventRaisingException e) {
            throw new XREObjectPainterException(
                "Error occurred while raising event 'onReady' event for XREImage.", e);
        }
    }

    /**
     * Get loaded bitmap.
     *
     * @param receiver the current receiver
     * @param imageUrl the image url
     * @return null or loaded bitmap
     * @throws XREObjectPainterException if exception occurs
     */
    public static Bitmap getBitmap(String imageUrl) throws XREObjectPainterException {
        ImageData imageData = imageMap.get(imageUrl);
        if (imageData == null) {
        //    throw new XREObjectPainterException("Image '" + imageUrl + "' is not loaded.");
        	return null;
        }
        return imageData.getBitmap();
    }

    public static void cleanUp() {
    	pendingImageUrls.clear();
    	imageMap.clear();
    }
    
    @SuppressWarnings("unchecked")
	public static void raiseImageEvents(AndroidReceiver receiver) throws XREObjectPainterException {
    	ApplicationData appData = receiver.getApplicationData();
    	if (appData == null) {
    		return;
    	}
    	
    	Map<String, Object> extraData = appData.getExtraPainterData();
    	if (extraData == null) {
    		return;
    	}
    	
    	List<XREImage> xreImages = (List<XREImage>) extraData.get("pendingImages");
    	if (xreImages == null) {
    		return;
    	}
    	
    	boolean exist = false;
    	
    	// collect loaded images
    	List<XREImage> loadedImages = new ArrayList<XREImage>();
    	for (XREImage xreImage : xreImages) {
    		if (imageMap.containsKey(xreImage.getUrl())) {
    			ImageData imageData = imageMap.get(xreImage.getUrl());
    			if (imageData.isLoadComplete()) {
    				loadedImages.add(xreImage);
    			}
    		}
    	}
    	
    	for (XREImage loadedImage : loadedImages) {
    		// raise onReady event
            XREResourceEvent event = new XREResourceEvent();
            event.setName(XREResourceEvent.ON_READY_EVENT_NAME);
            event.setSource(loadedImage.getId());
            
            try {
                AndroidXREView.getEventRaiser().raiseEvent(event, receiver, true);
            } catch (EventRaisingException e) {
                throw new XREObjectPainterException(
                    "Error occurred while raising event 'onReady' event for XREImage.", e);
            }
            
            xreImages.remove(loadedImage);
            
            exist = true;
    	}
    	
    	if (exist) {
    		receiver.getRootView().postInvalidate();
    	}
    }
    
    /**
     * Create {@link Typeface} from font.
     *
     * @param font the {@link XREFont} to use
     * @return create {@link Typeface}
     */
    public static Typeface createTypeface(XREFont font) {
        // get font style
        int style = 0;
        if (XREFont.STYLE_NORMAL.equals(font.getStyle())) {
            style = Typeface.NORMAL;
        } else if (XREFont.STYLE_BOLD.equals(font.getStyle())) {
            style = Typeface.BOLD;
        } else if (XREFont.STYLE_BOLDITALIC.equals(font.getStyle())) {
            style = Typeface.BOLD_ITALIC;
        } else if (XREFont.STYLE_ITALIC.equals(font.getStyle())) {
            style = Typeface.ITALIC;
        }

        // get typeface
        Typeface typeFace = Typeface.DEFAULT;
        if ("with-serif".equals(font.getFamily())) {
            typeFace = Typeface.SERIF;
        } else if ("without-serif".equals(font.getFamily())) {
            typeFace = Typeface.SANS_SERIF;
        } else if ("monospace".equals(font.getFamily())) {
            typeFace = Typeface.MONOSPACE;
        }

        // set typeface and style
        return Typeface.create(typeFace, style);
    }

    /**
     * Create {@link Paint} object form resource properties.
     *
     * @param xreText the {@link XREText}
     * @return created {@link Paint} object
     */
    public static Paint createPaint(XREText xreText) {
        // create a Paint instance according to the font and size of the text
        Paint paint = new Paint();

        // create font according to the given family and style if needed
        XREFont font = xreText.getFont();
        if (font != null) {
            paint.setTypeface(createTypeface(font));
        }

        // set font color and text size
        paint.setTextSize(xreText.getSize());
        paint.setColor(xreText.getColor());

        return paint;
    }

    /**
     * Gets last space in line.
     * <p>
     * AndroidAssembly2 added support for words larger than line
     *
     * @param line the line that is broken
     * @param breakCount the break position
     * @return the position of last space before break
     */
    public static int findLastSpace(String line, int breakCount) {
        if (breakCount == line.length()) {
            return breakCount;
        }

        // find last white space in line to break at that position
        boolean noWhitespace = false;
        int lastSpace = breakCount;
        while (!Character.isWhitespace(line.charAt(lastSpace))) {
            if (lastSpace == 0) {
                noWhitespace = true;
                break;
            }
            lastSpace--;
        }

        // if line has no white spaces
        if (noWhitespace) {
            return breakCount;
        }
        return lastSpace;
    }

    /**
     * Wrap lines accordingly to resource options.
     * <p>
     * AndroidAssembly3 new refactored method
     *
     * @param options the {@link XREResourceOptions}
     * @param lines the lines of text
     * @param paint the paint object
     * @param width the width of the view
     * @param startIntent the start intent of the text
     * @param endIntent the end intent of the text
     */
    public static void wrapLines(XREResourceOptions options, List<String> lines, Paint paint, int width,
        int startIntent, int endIntent) {
        if (options != null && options.getTextWrap() == XRETextWrap.WRAP) {
            int i = 0;
            while (i < lines.size()) {
                String line = lines.get(i);

                // break text
                int curLineWidth = getCurrentLineWidth(width, startIntent, endIntent, lines, i);
                int breakCount = paint.breakText(line, true, curLineWidth, null);
                // AndroidAssembly3 do not break text at 0 position
                if (breakCount != 0 && breakCount < line.length()) {
                    breakCount = findLastSpace(line, breakCount);

                    lines.set(i, line.substring(0, breakCount));
                    // skip space here
                    lines.add(i + 1, line.substring(breakCount + 1));
                }
                i++;
            }
        }
    }

    /**
     * Gets width of specified line.
     *<p>
     * AndroidAssembly3 new method
     *
     * @param width the width of the view
     * @param startIntent the start intent of at first line
     * @param endIntent the end intent at last line
     * @param lines the lines of test
     * @param i the index of current line
     * @return the width of current line
     */
    public static int getCurrentLineWidth(int width, int startIntent, int endIntent, List<String> lines, int i) {
        int curLineWidth = width;
        if (i == 0) {
            curLineWidth -= startIntent;
        }
        if (i == lines.size() - 1) {
            curLineWidth -= endIntent;
        }
        return curLineWidth;
    }

    /**
     * Thread that will load image.
     *
     * @author 7realm
     * @version 1.0
     */
    private static class ImageLoadingThread extends Thread {

        /** Represents logger that will be used to log information. */
        private static final Logger logger = LogUtil.getLogger(ImageLoadingThread.class.getSimpleName());

        static int totalBytes = 0;
        
        /**
         * Constructor for class ImageLoadingThread.
         *
         * @param imageUrl the image url
         * @param receiver the current receiver
         */
        private ImageLoadingThread() {
            imageLoadingThreadCount.incrementAndGet();
        }

        /**
         * This method is called when the loading thread is started. The thread is started when any {@link XREImage} is
         * created.
         */
        @Override
        public void run() {
            String methodName = "run";
            LogUtil.enter(logger, this, methodName);

            while (!pendingImageUrls.isEmpty()) {
	            try {
	        		final String imageUrl = pendingImageUrls.remove(0);
	        		
	        		HttpGet httpRequest = new HttpGet(imageUrl);
	
	                LogUtil.info(logger, this, methodName, "Start loading image from {0}", imageUrl);
	
	                ImageData imageData = imageMap.get(imageUrl);
	                if (imageData == null) {
	                	continue;
	                }
	                
	                HttpClient httpclient = new DefaultHttpClient();
	                HttpResponse response = httpclient.execute(httpRequest);
	                
	                HttpEntity entity = response.getEntity();
	                if (entity == null) {
	                    LogUtil.warn(logger, this, methodName, "Received no response from {0}", imageUrl);
	                } else {
	                    BufferedHttpEntity bufHttpEntity = new BufferedHttpEntity(entity);
	                    BufferedInputStream input = new BufferedInputStream(bufHttpEntity.getContent());
	
	                    try {
	                    	ByteArrayOutputStream imageContent = new ByteArrayOutputStream();
	                    	int ch = -1;
	                    	while ((ch = input.read()) != -1) {
	                    		imageContent.write(ch);
	                    	}
	                    	imageData.setBitmapData(imageContent.toByteArray());
	                    	//byte[] bitmapData = imageContent.toByteArray();
	                    	
	                    	//totalBytes += bitmapData.length;
	                    	//LogUtil.info(logger, this, methodName, "Creating image from {0} bytes, total bytes: {1}", bitmapData.length, totalBytes);
	                    	//Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length);
	                    	//imageData.setBitmap(bitmap);
	                    } finally {
	                        try {
	                            input.close();
	                        } catch (IOException e) {
	                            // simply ignore
	                        }
	                    }
	                }
	                imageData.setLoadComplete(true);
	                if (imageData.getReceiver() != null) {
	                	imageData.getReceiver().getRootView().postInvalidate();
	                }
	                
	            } catch (MalformedURLException e) {
	                LogUtil.error(logger, this, methodName, e);
	            } catch (UnknownServiceException e) {
	                LogUtil.error(logger, this, methodName, e);
	            } catch (IOException e) {
	                LogUtil.error(logger, this, methodName, e);
	            }
            }

            imageLoadingThreadCount.decrementAndGet();
            
            LogUtil.exit(logger, this, methodName);
        }
    }
}
